package yolo

import (
	"appengine"
	"appengine/datastore"
	"appengine/taskqueue"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"sort"
	"strings"
	"time"
)

type ActivityNotifier func(c appengine.Context, a *ActivityData)
var activityNotifiers = map[string]ActivityNotifier{} // type -> notifier

func init() {
	http.HandleFunc("/admin/post_activity_data", postActivityData)
	http.HandleFunc("/worker/store_activity_data", storeActivityData)
}

type ActivityData struct {
	Activity
	ActivityRecipients
	ActivityFiles
}

func RecordActivityData(c appengine.Context, activity *ActivityData) error {
	activities := make([]*ActivityData, 0, 1)
	activities = append(activities, activity)
	return RecordActivityDataMulti(c, activities)
}

// Single interface to store all activities.
func RecordActivityDataMulti(c appengine.Context, src []*ActivityData) error {
	var keys []*datastore.Key
	var activities []interface{}

	for _, a := range src {
		a.User = sanitizeEmail(a.User)
		AddUser(c, a.User)
		if a.Time.Unix() == 0 {
			a.Time = time.Now()
		}
		if a.SiteURL != "" {
			// Make sure we have no http:// part.
			a.SiteURL = strings.Replace(a.SiteURL, "http:", "", 1)
			a.SiteURL = strings.Replace(a.SiteURL, "/", "", -1)
		}
		if a.IssueOwner != "" {
			a.IssueOwner = sanitizeEmail(a.IssueOwner)
			AddUser(c, a.IssueOwner)
		}
		activities = append(activities, &a.Activity)
		keys = append(keys, a.Activity.Key(c))

		if len(a.Recipients) > 0 {
			for i, r := range a.Recipients {
				// Don't sanitize email addresses here: probably we want to
				// distinguish foo@ and foo+cc@ here.
				a.Recipients[i] = r
				AddUser(c, r)
			}
			a.ActivityRecipients.ActivityTime = a.Time
			activities = append(activities, &a.ActivityRecipients)
			keys = append(keys, a.ActivityRecipients.Key(c, &a.Activity))
		}

		if len(a.Files) > 0 && len(a.Files) <= maxActivityFiles {
			a.ActivityFiles.ActivityTime = a.Time
			activities = append(activities, &a.ActivityFiles)
			keys = append(keys, a.ActivityFiles.Key(c, &a.Activity))
			if a.Type == ActivityCommit {
				IncrementUserCommits(c, a.User, a.Files)
			}
		}
		if an, found := activityNotifiers[a.Type]; found {
			an(c, a)
		}
	}

	_, err := datastore.PutMulti(c, keys, activities)
	if err != nil {
		c.Errorf("%s: Storing activity failed: %s", currentLocation(), err.Error())
	}

	as := []*Activity{}
	emails := []string{}
	for _, a := range src {
		as = append(as, &a.Activity)
		emails = append(emails, a.User)
	}
	AddUserActivities(c, emails, as)

	return err
}

//----------------------------------------------------------------------------
const (
	ActivityOther = "Other"
	ActivityComment = "Comment"
	ActivityApprove = "Approve"
	ActivityCommit  = "Commit"
)

const (
	maxActivitySnippetLength = 400
	maxActivityFiles = 30
)

type Activity struct {
	User    string
	Time time.Time
	URL     string
	Snippet string
	Type    string
	SiteURL string

	// For issue-related activities.
	Issue      string  // siteURL/issue
	IssueOwner string
	CommentId  int     // issue-local id (assumes 1-origin)
}

func (a* Activity) Key(c appengine.Context) *datastore.Key {
	// Make sure we create an identifiable key so that we will not be storing
	// duplicated activities for the same one.
	id := fmt.Sprintf("%s-%v-%s-%s", a.User, a.Time.Unix(), a.Type, a.URL)
	return datastore.NewKey(c, "Activity", id, 0, nil)
}

// Parent: Activity
// Key: User
type ActivityRecipients struct {
	ActivityTime time.Time
	Recipients []string
}

func (ar* ActivityRecipients) Key(c appengine.Context, a *Activity) *datastore.Key {
	return datastore.NewKey(c, "ActivityRecipients", a.User, 0, a.Key(c))
}

// Parent: Activity
// Key: User
type ActivityFiles struct {
	ActivityTime time.Time
	Files []string
}

func (af* ActivityFiles) Key(c appengine.Context, a* Activity) *datastore.Key {
	return datastore.NewKey(c, "ActivityFiles", a.User, 0, a.Key(c))
}

// For activity sort ---------------------------------------------------------

type ActivityArray []interface{}

func (a ActivityArray) Len() int {
	return len([]interface{}(a))
}

func (a ActivityArray) Less(i, j int) bool {
	ai := ([]interface{}(a)[i]).(*Activity)
	aj := ([]interface{}(a)[j]).(*Activity)
	return ai.Time.After(aj.Time)
}

func (a ActivityArray) Swap(i, j int) {
	[]interface{}(a)[i], []interface{}(a)[j] = []interface{}(a)[j], []interface{}(a)[i]
}

// Getters -------------------------------------------------------------------
func GetActivities(c appengine.Context, email, issue string, offset, limit int) ([]interface{}, error) {
	if email != "" {
		_, err := GetUser(c, email)
		// try obfscated one.
		if err != nil {
			_, err = GetUser(c, obfscateEmail(email))
		}
		c.Debugf("%v%v",  email, err)
		if err != nil {
			var activities []interface{}
			return activities, nil
		}
	}

	q := datastore.NewQuery("Activity").Order("-Time").Offset(offset).Limit(limit)
	if email != "" {
		q = q.Filter("User =", email)
	}
	if issue != "" {
		q = q.Filter("Issue =", issue)
	}
	var activities []Activity
	_, err := q.GetAll(c, &activities)

	// Since GO lang app engine might no have 'IN', following workaround is needed but not perfect.
	var activities_obfscated []Activity
	if email != "" {
		q := datastore.NewQuery("Activity").Order("-Time").Offset(offset).Limit(limit)
		q = q.Filter("User =", obfscateEmail(email))
		q.GetAll(c, &activities_obfscated)
	}

	var items = make([]interface{}, 0, len(activities) + len(activities_obfscated))
	for i, _ := range activities {
		items = append(items, &activities[i])
	}
	for i, _ := range activities_obfscated {
		items = append(items, &activities_obfscated[i])
	}

	return items, err
}

func GetTimeline(c appengine.Context, email string, offset, limit int) ([]interface{}, error) {
	ch := make(chan error)
	var results struct {
		recipients []interface{}
		activities []interface{}
	}

	go func() {
		q := datastore.NewQuery("ActivityRecipients").Order("-ActivityTime").Filter("Recipients =", email).Offset(offset).Limit(limit).KeysOnly()
		keys, err := q.GetAll(c, nil)
		if err != nil {
			ch <- err
			return
		}
		results.recipients = make([]interface{}, len(keys))
		for i, k := range keys {
			keys[i] = k.Parent()
			results.recipients[i] = new(Activity)
		}
		err = datastore.GetMulti(c, keys, results.recipients)
		ch <- err
	}()

	go func() {
		var err error
		results.activities, err = GetActivities(c, email, "", offset, limit)
		ch <- err
	}()

	var err error = nil
	for i := 0; i < 2; i++ {
		e := <-ch
		if e != nil {
			err = e
			c.Errorf("%s: Got error: %s", currentLocation(), err.Error())
		}
	}

	if err != nil {
		var activities []interface{}
		return activities, nil
	}

	activities := results.activities
	activities = append(activities, results.recipients...)

	sort.Sort(ActivityArray(activities))
	return activities, err
}

func GetFileActivities(c appengine.Context, file string, offset, limit int) ([]interface{}, error) {
	q := datastore.NewQuery("ActivityFiles").Order("-ActivityTime").Filter("Files =", file).Offset(offset).Limit(limit).KeysOnly()
	keys, err := q.GetAll(c, nil)
	if err != nil {
		var activities []interface{}
		return activities, nil
	}
	c.Infof("Got: %v", len(keys))
	activities := make([]interface{}, len(keys))
	for i, k := range keys {
		keys[i] = k.Parent()
		activities[i] = new(Activity)
	}
	err = datastore.GetMulti(c, keys, activities)
	return activities, err
}

func GetUserActivityCounts(c appengine.Context, email string) ([]*CountMap, error) {
	q := datastore.NewQuery("ActivityRecipients").Order("-ActivityTime").Filter("Recipients =", email).Limit(100).KeysOnly()
	keys, err := q.GetAll(c, nil)
	if err != nil {
		return nil, err
	}
	var perUserCount = make(map[string]int64)
	for _, k := range keys {
		perUserCount[k.StringID()] += 1
	}

	var array CountMapArray
	array.counts = make([]*CountMap, 0, len(perUserCount))
	for k, v := range perUserCount {
		array.counts = append(array.counts, &CountMap{k, v})
	}
	sort.Sort(&array)

	return array.counts, nil
}

func GetUserFileActivityCounts(c appengine.Context, file string) ([]*CountMap, error) {
	q := datastore.NewQuery("ActivityFiles").Order("-ActivityTime").Filter("Files =", file).Limit(100).KeysOnly()
	keys, err := q.GetAll(c, nil)
	if err != nil {
		return nil, err
	}
	var perUserCount = make(map[string]int64)
	for _, k := range keys {
		perUserCount[k.StringID()] += 1
	}

	var array CountMapArray
	array.counts = make([]*CountMap, 0, len(perUserCount))
	for k, v := range perUserCount {
		array.counts = append(array.counts, &CountMap{k, v})
	}
	sort.Sort(&array)

	return array.counts, nil
}

//--------------------------------------------------------------------------
func postActivityData(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	d := json.NewDecoder(r.Body)
	var activities []ActivityData
	err := d.Decode(&activities)
	if err != nil {
		c.Warningf("%s: decode err:%s", currentLocation(), err)
		return
	}
	for _, activity := range activities {
		c.Debugf("%s: will store %s", currentLocation(), activity.Activity.Key(c))
		a, _ := json.Marshal(activity)
		params := map[string][]string{"activity": {string(a)}}
		t := taskqueue.NewPOSTTask("/worker/store_activity_data", params)
		if _, err := taskqueue.Add(c, t, "default"); err != nil {
			c.Errorf("taskqueue.Add failed: %s", err.Error())
		}
	}
}

func storeActivityData(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	a := r.FormValue("activity")
	var activity ActivityData
	err := json.Unmarshal([]byte(a), activity)
	if err != nil {
		c.Warningf("put unmarshal failed: %s", err)
		return
	}
	RecordActivityData(c, &activity)
}

// To show obfscated email address.
var (
	obfscatedEmailRegexp = regexp.MustCompile(`...@`)
)
func obfscateEmail(email string) (obfscated string) {
	obfscated = obfscatedEmailRegexp.ReplaceAllString(email, "...@")
	return
}
